import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

public class MyDedupIndex {
    public static String INDEX_FILE = "mydedup.index";
    Map<String, List<String>> pathName2checksum = new HashMap<>();
    Map<String, Set<String>> container2checksum = new HashMap<>();
    Map<String, Chunk> checksum2chunk = new HashMap<>();

    @JsonIgnore
    public Container container = new Container();

//    Set<String> checksums;
    int fileCount =0;
    int totalChunks=0;
    int uniqueChunks=0;
    int totalBytes=0;
    int uniqueBytes=0;
    int totalContainers=0;

    public boolean containFile(String pathName) {
        return pathName2checksum.containsKey(pathName);
    }

    public Chunk addChunk(String pathName, byte[] b, int len){
        if (!pathName2checksum.containsKey(pathName)) {  // It must enter this branch.
            pathName2checksum.put(pathName, new ArrayList<>(1));  // index a file
            fileCount += 1;
        }

        String checksum = ChecksumCalculator.calcChecksum(b, 0, len);
        totalChunks += 1;
        totalBytes += len;
        Chunk chunk;
        if (!checksum2chunk.containsKey(checksum)) {  // new chunk
            uniqueChunks += 1;
            uniqueBytes += len;
            chunk = container.addChunk(b, len);  // packing to container
            chunk.relativeFiles.add(pathName);  // index the chunk relate to which files
            checksum2chunk.put(checksum, chunk);  // index chunk
            if (!container2checksum.containsKey(container.name)) {
                container2checksum.put(container.name, new HashSet<>());
            }
            container2checksum.get(container.name).add(checksum);  // index container and chunks whose in it
        }else{
            chunk = checksum2chunk.get(checksum);  // return chunk info from index file
        }
        pathName2checksum.get(pathName).add(checksum);  // index checksum into file
        totalContainers = container.total; // refresh data
        return chunk;
    }
    public Chunk addLastChunk(String pathName, byte[] b, int len){
        Chunk chunk = addChunk(pathName, b, len);
        container.flush();
        totalContainers = container.total; // refresh data
        return chunk;
    }

    public void printStatics(){
        System.out.println("Total number of files that have been stored: " + fileCount);
        System.out.println("Total number of pre-deduplicated chunks in storage: " + totalChunks);
        System.out.println("Total number of unique chunks in storage: " + uniqueChunks);
        System.out.println("Total number of bytes of pre-deduplicated chunks in storage: " + totalBytes);
        System.out.println("Total number of bytes of unique chunks in storage: " + uniqueBytes);
        System.out.println("Total number of containers in storage: " + totalContainers);
        System.out.printf("Deduplication ratio: %.2f\n" , totalBytes*1.0/uniqueBytes);
    }

    void delete(String pathName) {
        List<String> checksums = pathName2checksum.get(pathName);
        if (checksums == null) {
            System.out.println(MyDedupIndex.INDEX_FILE + " doesn't contains " + pathName);  // should not be shown
            return;
        }
        pathName2checksum.remove(pathName);  // clean file(pathName2checksum)
        fileCount--;
        totalChunks -= checksums.size();
        checksums.stream().map(t->checksum2chunk.get(t)).forEach(t->{
            totalBytes -= t.size;
        });
        for (String checksum : checksums) {
            Chunk chunk = checksum2chunk.get(checksum);
            if(chunk == null){ // the chunk is shown in previous part of pathName file, no need to do with the chunk
                continue;
            }
            chunk.relativeFiles.remove(pathName);
            if (chunk.relativeFiles.isEmpty()) {  // invalid chunk
                checksum2chunk.remove(checksum); // clean chunk(checksum2chunk)
                uniqueChunks--;
                uniqueBytes -= chunk.size;
                container2checksum.get(chunk.container).remove(checksum); // clean container(container2checksum)
                if (container2checksum.get(chunk.container).isEmpty()) {
                    container2checksum.remove(chunk.container); // delete container in index file
                    totalContainers--;
                    container.total--;
                    File containerFile = Paths.get(Container.DATA_DIR,chunk.container).toFile();
                    containerFile.delete();  // delete container in storage physically
                    if (container.mode.equals("azure")) {
                        container.azureClient.delete(chunk.container);
                    }
                }
            }
        }
    }
    boolean validateChunking() {
        try {
            for (Map.Entry<String, List<String>> entry : pathName2checksum.entrySet()) {
                byte[] wholeFile = Files.readAllBytes(Paths.get(entry.getKey()));

                int visited = 0;
                for (String s : entry.getValue()) {
                    Chunk chunk = checksum2chunk.get(s);
                    byte[] chunkBytes = container.readChunkBytesFromContainer(chunk);
                    for (int i = 0; i < chunkBytes.length; i++) {
                        if (chunkBytes[i] != wholeFile[visited + i]) {
                            return false;
                        }
                    }
                    visited += chunkBytes.length;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }
    public void saveIndex() {
//        if (validateChunking()) {
//            System.out.println("chunking's consistency check pass! ");
//        }else{
//            System.out.println("chunking's consistency check fail! ");
//        }
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        ObjectWriter objectWriter = objectMapper.writer().withDefaultPrettyPrinter();
        try {
            objectWriter.writeValue(new File(INDEX_FILE), this);
        } catch (IOException e) {
            System.out.println("saveIndex failed.");
            e.printStackTrace();
        }
    }

    public void setContainerMode(String mode) {
        container.mode = mode;
    }

    public static MyDedupIndex loadIndex() {
        File indexFile = new File(INDEX_FILE);
        ObjectMapper objectMapper = new ObjectMapper();
        MyDedupIndex myDedupIndex = new MyDedupIndex();
        try {
            myDedupIndex = objectMapper.readValue(indexFile, MyDedupIndex.class);
        } catch (IOException e) {
//            e.printStackTrace();
            System.out.println("loadIndex failed. use empty file");
            myDedupIndex = new MyDedupIndex();
        } finally {
            myDedupIndex.container.total = myDedupIndex.totalContainers;
            return myDedupIndex;
        }
    }

    public Map<String, List<String>> getPathName2checksum() {
        return pathName2checksum;
    }

    public void setPathName2checksum(Map<String, List<String>> pathName2checksum) {
        this.pathName2checksum = pathName2checksum;
    }

    public int getFileCount() {
        return fileCount;
    }

    public void setFileCount(int fileCount) {
        this.fileCount = fileCount;
    }

    public int getTotalChunks() {
        return totalChunks;
    }

    public void setTotalChunks(int totalChunks) {
        this.totalChunks = totalChunks;
    }

    public int getUniqueChunks() {
        return uniqueChunks;
    }

    public void setUniqueChunks(int uniqueChunks) {
        this.uniqueChunks = uniqueChunks;
    }

    public int getTotalBytes() {
        return totalBytes;
    }

    public void setTotalBytes(int totalBytes) {
        this.totalBytes = totalBytes;
    }

    public int getUniqueBytes() {
        return uniqueBytes;
    }

    public void setUniqueBytes(int uniqueBytes) {
        this.uniqueBytes = uniqueBytes;
    }

    public int getTotalContainers() {
        return totalContainers;
    }

    public void setTotalContainers(int totalContainers) {
        this.totalContainers = totalContainers;
    }

    public static String getIndexFile() {
        return INDEX_FILE;
    }

    public static void setIndexFile(String indexFile) {
        INDEX_FILE = indexFile;
    }

    public Map<String, Chunk> getChecksum2chunk() {
        return checksum2chunk;
    }

    public void setChecksum2chunk(Map<String, Chunk> checksum2chunk) {
        this.checksum2chunk = checksum2chunk;
    }

    public Container getContainer() {
        return container;
    }

    public void setContainer(Container container) {
        this.container = container;
    }

    public Map<String, Set<String>> getContainer2checksum() {
        return container2checksum;
    }

    public void setContainer2checksum(Map<String, Set<String>> container2checksum) {
        this.container2checksum = container2checksum;
    }
}

